package ar.uba.tleng.g99.expregs.automata;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import ar.uba.tleng.g99.expregs.parser.Expr;

/**
 * Automata no deterministico (AFND).
 * 
 * @author g99
 */
public final class AutomataND {

	/**
	 * Devuelve un automata no deterministico creado a partir de una Expr.
	 * 
	 * @param sigma
	 *            el alfabeto.
	 * @param expr
	 *            la Expr evaluada de una expresion regular.
	 * @return el automata no deterministico.
	 */
	public static AutomataND fromExpr(Set<Character> sigma, Expr expr) {
		return expr.automata(sigma);
	}

	public static AutomataND or(Set<Character> sigma, AutomataND a1,
			AutomataND a2) {
		AutomataND automata1 = a1.copy();
		AutomataND automata2 = a2.copy();
		checkSigma(sigma, automata1);
		checkSigma(sigma, automata2);
		Node ini = new Node();
		Node end = new Node();
		ini.addTransition(Node.LAMBDA, a1.getInitialNode());
		ini.addTransition(Node.LAMBDA, a2.getInitialNode());
		for (Node n : a1.getFinalNodes()) {
			n.addTransition(Node.LAMBDA, end);
		}
		for (Node n : a2.getFinalNodes()) {
			n.addTransition(Node.LAMBDA, end);
		}
		return new AutomataND(sigma, ini, Collections.singleton(end));
	}

	public static AutomataND concat(Set<Character> sigma, AutomataND a1,
			AutomataND a2) {
		AutomataND automata1 = a1.copy();
		AutomataND automata2 = a2.copy();
		checkSigma(sigma, automata1);
		checkSigma(sigma, automata2);
		Node ini = new Node();
		Node end = new Node();
		ini.addTransition(Node.LAMBDA, a1.getInitialNode());
		for (Node n : a1.getFinalNodes()) {
			n.addTransition(Node.LAMBDA, a2.getInitialNode());
		}
		for (Node n : a2.getFinalNodes()) {
			n.addTransition(Node.LAMBDA, end);
		}
		return new AutomataND(sigma, ini, Collections.singleton(end));
	}

	public static AutomataND question(Set<Character> sigma, AutomataND a) {
		AutomataND automata = a.copy();
		checkSigma(sigma, automata);
		return or(sigma, lambdaTransition(sigma), automata);
	}

	public static AutomataND asterisk(Set<Character> sigma, AutomataND a) {
		AutomataND automata = a.copy();
		checkSigma(sigma, automata);
		Node ini = new Node();
		Node end = new Node();
		ini.addTransition(Node.LAMBDA, end);
		ini.addTransition(Node.LAMBDA, automata.getInitialNode());
		for (Node n : automata.getFinalNodes()) {
			n.addTransition(Node.LAMBDA, end);
		}
		end.addTransition(Node.LAMBDA, ini);
		return new AutomataND(sigma, ini, Collections.singleton(end));
	}

	public static AutomataND plus(Set<Character> sigma, AutomataND a) {
		AutomataND automata = a.copy();
		checkSigma(sigma, automata);
		return concat(sigma, automata, asterisk(sigma, automata));
	}

	private static void checkSigma(Set<Character> sigma, AutomataND wrapped) {
		if (!sigmaCompatible(sigma, wrapped.getSigma())) {
			throw new RuntimeException("Incompatible sigma operation");
		}
	}

	private static boolean sigmaCompatible(Set<Character> wrapper,
			Set<Character> wrapped) {
		if (wrapper == wrapped) {
			return true;
		}
		return wrapper.containsAll(wrapped);
	}

	/**
	 * Crea un automata de transicion simple.
	 * 
	 * @param c
	 *            el caracter de transicion.
	 * @return el afnd creado.
	 */
	public static AutomataND simpleTransition(Set<Character> sigma, char c) {
		if (c == '.') {
			return dotTransition(sigma);
		}
		if (c != Node.LAMBDA && !sigma.contains(c)) {
			throw new RuntimeException("Sigma does not contain char: " + c);
		}
		Node ini = new Node();
		Node end = new Node();
		ini.addTransition(c, end);
		return new AutomataND(sigma, ini, Collections.singleton(end));
	}

	public static AutomataND lambdaTransition(Set<Character> sigma) {
		return simpleTransition(sigma, Node.LAMBDA);
	}

	public static AutomataND dotTransition(Set<Character> sigma) {
		Node ini = new Node();
		Node end = new Node();
		for (Character c : sigma) {
			ini.addTransition(c, end);
		}
		return new AutomataND(sigma, ini, Collections.singleton(end));
	}

	private final Node initialNode;
	private final Set<Node> finalNodes;
	private final Set<Character> sigma;

	public AutomataND(Set<Character> sigma, Node initialNode,
			Set<Node> finalNodes) {
		this.sigma = Collections.unmodifiableSet(new HashSet<Character>(sigma));
		this.initialNode = initialNode;
		this.finalNodes = new HashSet<Node>(finalNodes);
	}

	public Node getInitialNode() {
		return initialNode;
	}

	public Set<Node> getFinalNodes() {
		return finalNodes;
	}

	public Set<Character> getSigma() {
		return sigma;
	}

	public AutomataND copy() {
		NodeCopier copier = new NodeCopier(this.initialNode);
		Node ini = copier.translate(this.initialNode);
		return new AutomataND(this.getSigma(), ini,
				copier.translateMany(this.finalNodes));
	}
}
